package org.goobs.mapping;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

import org.goobs.database.Database;
import org.goobs.database.GenericDatabaseRow;
import org.goobs.database.ResultFactory;
import org.goobs.database.Table;
import org.goobs.utils.Pair;
import org.goobs.utils.ProgressBar;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public class RoadMap {

	private static final double FUDGE = 0.1;
	
	private Database db;

	public RoadMap(Database db) throws SAXException, MappingException {
		this.db = db;
		ensureDatabase();
	}

	private void ensureDatabase() {
		//--Connect
		if (!db.connect()) {
			throw new MappingException("Could not connect to database");
		}

		//--Ensure node table
		if (db.getTable("node") == null) {
			if (!db.createTable("node", Location.COLS, Location.TYPES, Location.INDEX, false)) {
				// throw new
				// MappingException("Could not create 'nodes' database");
			}
		}
		
		//--Ensure node tag table
		if (db.getTable("node_tag") == null) {
			if (!db.createTable("node_tag", Tag.COLS, Tag.TYPES, Tag.INDEX, false)) {
				// throw new
				// MappingException("Could not create 'nodes' database");
			}
		}
		
		//--Ensure the way table
		if (db.getTable("way") == null) {
			if (!db.createTable("way", RoadNode.COLS, RoadNode.TYPES, RoadNode.INDEX, false)) {
				// throw new
				// MappingException("Could not create 'nodes' database");
			}
		}
		
		//--Ensure the way tag table
		if (db.getTable("way_tag") == null) {
			if (!db.createTable("way_tag", Tag.COLS, Tag.TYPES, Tag.INDEX, false)) {
				// throw new
				// MappingException("Could not create 'nodes' database");
			}
		}

	}

	private void refreshServer() {
		// TODO do something here about out of memory exceptions
	}

	public void parse(Document doc, boolean skipIfExists) {
		// ---
		// Overhead
		// ---
		// --Database
		Table nodes = db.getTable("node");
		Table nodeTags = db.getTable("node_tag");
		Table ways = db.getTable("way");
		Table wayTags = db.getTable("way_tag");
		if (nodes == null || nodeTags == null || ways == null
				|| wayTags == null) {
			throw new MappingException(
					"Could not get relevant tables in database!");
		}
		// --Variables
		// (xml)
		doc.getDocumentElement().normalize();
		NodeList nodeList = doc.getElementsByTagName("node");
		System.out.println("node count: " + nodeList.getLength());
		NodeList wayList = doc.getElementsByTagName("way");
		System.out.println("way count: " + wayList.getLength());
		// (database rows)
		Location loc = new Location();
		RoadNode roadNode = new RoadNode();
		Tag tg = new Tag(0, null, null);

		// ---
		// Nodes
		// ---
		System.out.println("Nodes:");
		ProgressBar bar = new ProgressBar(nodeList.getLength(), 100);
		for (int n = 0; n < nodeList.getLength(); n++) {
			if (bar.tick()) {
				refreshServer();
			}
			// --Parse basic information
			Element location = null;
			int id;
			double lat;
			double lon;
			try {
				location = (Element) nodeList.item(n);
				id = Integer.parseInt(location.getAttribute("id"));
				lat = Double.parseDouble(location.getAttribute("lat"));
				lon = Double.parseDouble(location.getAttribute("lon"));
			} catch (RuntimeException e) {
				throw new MappingException("Malformed node tag at index " + n);
			}
			// --Check if it exists in the database
			if (skipIfExists && nodes.where("osm_node_id=" + id).hasMoreRows()) {
				System.out.println("skipping node " + id);
				continue;
			}
			// --Parse special tags
			int place = Location.NONE;
			String name = "";
			NodeList tags = location.getElementsByTagName("tag");
			for (int t = 0; t < tags.getLength(); t++) {
				// (get tags)
				Element tag = (Element) tags.item(t);
				String key = tag.getAttribute("k");
				String val = tag.getAttribute("v");
				if (key == null || val == null) {
					throw new MappingException(
							"Could not retrieve tag for node with reference: "
									+ id);
				}
				if (key.equals("created_by")) {
					// do nothing (ignore these)
				} else if (key.equals("converted_by")) {
					// do nothing (ignore these)
				} else {
					// (add the tag to the generic tag table)
					tg.recycle(id, key, val);
					nodeTags.addRow(tg);
				}
			}

			// --Add location data
			loc.recycle(lat, lon, id);
			nodes.addRow(loc);
		}

		// ---
		// Ways
		// ---
		System.out.println("Ways:");
		ProgressBar bar2 = new ProgressBar(wayList.getLength(), 100);
		for (int w = 0; w < wayList.getLength(); w++) {
			bar2.tick();
			// --Parse basic information
			Element way = (Element) wayList.item(w);
			int id = Integer.parseInt(way.getAttribute("id"));
			// --Check if it exists in database
			if (skipIfExists && ways.where("osm_way_id=" + id).hasMoreRows()) {
				System.out.println("skipping way " + id);
				continue;
			}
			// --Parse nodes
			NodeList nds = way.getElementsByTagName("nd");
			if (nds.getLength() == 0) {
				throw new MappingException(
						"Way found without any nodes. way id: " + id);
			}
			int ndIndex = 0;
			for (int ndI = 0; ndI < nds.getLength(); ndI++) {
				Element node = (Element) nds.item(ndI);
				int ref = Integer.parseInt(node.getAttribute("ref"));
				roadNode.recycle(id, ndIndex++, ref);
				ways.addRow(roadNode);
			}
			// --Parse special tags
			NodeList tags = way.getElementsByTagName("tag");
			for (int t = 0; t < tags.getLength(); t++) {
				// (get tags)
				Element tag = (Element) tags.item(t);
				String key = tag.getAttribute("k");
				String val = tag.getAttribute("v");
				if (key == null || val == null) {
					throw new MappingException(
							"Could not retreive tag for way with reference: "
									+ id);
				}
				// (add the tag to the generic tag table)
				if (key.equals("created_by")) {
					// do nothing (ignore these)
				} else if (key.equals("converted_by")) {
					// do nothing (ignore these)
				} else {
					tg.recycle(id, key, val);
					wayTags.addRow(tg);
				}
			}
		}
	}

	
	
	
	
	
	public Set<City> extractCityMap() {
		// --Get the database
		Table nodes = db.getTable("node");
		Table nodeTags = db.getTable("node_tag");
		Table ways = db.getTable("way");
		Table wayTags = db.getTable("way_tag");
		if (nodes == null || nodeTags == null || ways == null
				|| wayTags == null) {
			throw new MappingException("relvant database is not available");
		}

		// --- 
		// Get cities
		// ---
		Set<City> cities = new HashSet<City>();
		ResultFactory fact = db.join(nodes, nodeTags, "node.osm_node_id=node_tag.elem_id AND node_tag.k='place' AND node_tag.v='city'");

		GenericDatabaseRow joinResult = new GenericDatabaseRow();
		Tag tag = new Tag();
		while (fact.hasMoreRows()) {
			// (get city)
			if(!fact.fillObject(joinResult)){
				throw new MappingException("Could not recover city object!");
			}
			int id = (Integer) joinResult.getObject("osm_node_id");
			double longitude = (Double) joinResult.getObject("lon");
			double latitude = (Double) joinResult.getObject("lat");
			// (get tags for city)
			Map<String, String> tags = new HashMap<String, String>();
			ResultFactory tagFact = nodeTags.where("elem_id=" + id);
			while (tagFact.hasMoreRows()) {
				tagFact.fillObject(tag);
				tags.put(tag.getKey(), tag.getValue());
			}
			// (create a new city object)
			String pop = tags.get("population");
			int population = -1;
			if (pop != null) {
				try {
					population = Integer.parseInt(pop);
				} catch (NumberFormatException e) {
				}
			}
			String strName = tags.get("name");
			String name = "[unnamed]";
			if(strName != null){
				name = strName;
			}
			City city = new City(longitude, latitude, name,
					population);
//			System.out.println("found city: " + id + "\t:\t" + city);
			cities.add(city);
		}
		System.out.println("Retreived cities: " + cities.size());

		// ---
		// Get Roads
		// ---
		//--Get every highway
		Set <Integer> highways = new HashSet <Integer> ();
		fact = db.join(ways, wayTags, "way.osm_way_id=way_tag.elem_id AND way_tag.k='highway' AND " +
				" (way_tag.v='motorway' OR way_tag.v='trunk' )");
		if(!fact.hasMoreRows()){
			throw new MappingException("No highways to extract!");
		}
		while(fact.hasMoreRows()){
			if(!fact.fillObject(joinResult)){
				throw new MappingException("could not fill object");
			}
			Integer next = (Integer) joinResult.getObject("osm_way_id");
			if(next == null){
				throw new MappingException("join did not have a proper OSM_way_id");
			}else{
				highways.add(next);
			}
		}
		System.out.println("Retrieved highways: " + highways.size());
		
		//--Make road objects
		int rawNodeCount = 0;
		Map <String, Set <Road>> rawRoads = new HashMap <String, Set<Road>> ();
		for(Integer id : highways){
			//(set up join)
			String[] cols = {"node_index", "lat", "lon"};
			fact = db.join(ways, nodes, "node.osm_node_id=way.node_id AND osm_way_id=" + id, cols);
			if(!fact.hasMoreRows()){
				throw new MappingException("Unsuccessful join for getting road coordinates");
			}
			//(get coordinates)
			Map <Integer, double[]> cords = new HashMap<Integer, double[]>();
			while(fact.hasMoreRows()){
				if(!fact.fillObject(joinResult)){
					throw new MappingException("Unsuccessful join for getting road coordinates");
				}
				double[] cor = new double[2];
				int index = (Integer) joinResult.getObject("node_index");
				cor[0] = (Double) joinResult.getObject("lon");
				cor[1] = (Double) joinResult.getObject("lat");
				cords.put(index, cor);
			}
			//(create coordinate sequence)
			int index=0;
			double[] cor;
			double[] lons = new double[cords.keySet().size()];
			double[] lats = new double[cords.keySet().size()];
			while( (cor = cords.get(index)) != null){
				lons[index] = cor[0];
				lats[index] = cor[1];
				index++;
			}
			Road road = new Road(lons, lats);
			//(get road tags)
			fact = wayTags.where("elem_id=" + id);
			while(fact.hasMoreRows()){
				fact.fillObject(tag);
				road.setProperty(tag.getKey(), tag.getValue());
			}
			//(group roads)
			String ref = road.getProperty("ref");
			if(!rawRoads.containsKey(ref)){
				rawRoads.put(ref, new HashSet <Road> ());
			}
//			System.out.println("extracted road: " + road);
			rawNodeCount += road.numNodes();
			rawRoads.get(ref).add(road);
		}
		
		//--Handle double highways (e.g. "A1 ; A7")
		Set <String> toRemove = new HashSet <String> ();
		for(String ref : rawRoads.keySet()){
			if(ref.contains(";")){
				StringTokenizer tokens = new StringTokenizer(ref, ";");
				while(tokens.hasMoreTokens()){
					String singleRef = tokens.nextToken().trim();
					if(!rawRoads.containsKey(singleRef)){
						System.out.println("[RoadMap] WARNING: concatenated highway does not have single road counterparts");
						rawRoads.put(singleRef, new HashSet <Road> ());
					}
					Set <Road> shared = rawRoads.get(ref);
					Set <Road> singleHighway = rawRoads.get(singleRef);
					for(Road r : shared){
						singleHighway.add(r.copy());	
					}
				}
				toRemove.add(ref);
			}
		}
		for(String rm : toRemove){
			rawRoads.remove(rm);
		}	
		
		//--Merge highways
		//(merge)
		Set <Road> roads = new HashSet <Road> ();
		int hardMerge = 0;
		for( String ref : rawRoads.keySet() ){
			if(ref.equals("")){
				Set <Road> noname = rawRoads.get(ref);
				int dropped = 0;
				for(Road r : noname){
					dropped += r.numNodes();
				}
				System.out.println("Warning: dropping highways without reference. Roads dropped: " + rawRoads.get(ref).size() + " Nodes dropped: " + dropped);
				continue;
			}
			Set <Road> roadsInRef = rawRoads.get(ref);
			roadsInRef = Road.preciseMerge(roadsInRef);
			hardMerge += roadsInRef.size();
			roadsInRef = Road.softMerge(roadsInRef, FUDGE);
			for(Road r : roadsInRef){
				roads.add(r);
			}
		}
		//(analyze dropped node count)
		int actNodeCount = 0;
		for(Road r : roads){
			actNodeCount += r.numNodes();
		}
		System.out.println("Unique highways (hard merge): " + hardMerge);
		System.out.println("Unique highways (soft merge): " + roads.size());
		System.out.println("DROPPED NODES: " + (rawNodeCount - actNodeCount) + " = " + rawNodeCount + " - " + actNodeCount);
		
		
		// ---
		// Get Junctions
		// ---
		int junctionsAdded = 0;
		Set <Road> seenInOuterLoop = new HashSet <Road> ();
		for(Road r1 : roads){
			seenInOuterLoop.add(r1);
			for(Road r2 : roads){
				if(seenInOuterLoop.contains(r2)){
					continue;
				}
				if(r1.getProperty("ref").equals(r2.getProperty("ref"))){
					//TODO this should be an error!
					continue;
				}
				Pair <City, Double> pair = Road.getClosestJunction(r1, r2);
				if(pair.cdr() < FUDGE/10.0){
					cities.add(pair.car());
					junctionsAdded++;
				}
			}
		}
		System.out.println("Junctions added: " + junctionsAdded);
		
		
		// ---
		// Sweep Roads
		// ---
		for(Road r : roads){
			System.out.println("sweeping road: " + r);
			Set <IntercityRoad> motorways = r.sweep(cities);
			for(IntercityRoad road : motorways){
				road.getBegin().addRoad(road);
			}
		}

		return cities;
	}

	public boolean saveCityMap(Set<City> cities, File f) {
		try {
			FileWriter writer = new FileWriter(f);
			for(City c : cities){
				writer.append(c.getName()).append("\t");
			}
			for(City c : cities){
				for(IntercityRoad r : c.getRoads()){
					writer.append(r.getBegin().getName()).append("\t");
					writer.append(r.getEnd().getName()).append("\t");
					writer.append(new Double(r.getDistance()).toString()).append("\n");
				}
			}
			writer.flush();
			return true;
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		}
	}

	public boolean extractAndSaveCityMap(File f) {
		return saveCityMap(extractCityMap(), f);
	}
	
	public static boolean isClose(City c, double lon, double lat){
		return isClose(c.getLongitude(), c.getLatitude(), lat, lon);
	}

	public static boolean isClose(double lon1, double lat1, double lon2, double lat2){
		return Math.abs(lon2-lon1) < 0.5 && Math.abs(lat2-lat1) < 0.5;
	}
	
	public static double toKilometers(City c, double lon, double lat){
		return toKilometers(c.getLongitude(), c.getLatitude(), lon, lat);
	}
	
	public static double toKilometers(double lon1, double lat1, double lon2,
			double lat2) {
		double x = 69.1 * (lat2 - lat1);
		double y = 69.1 * (lon2 - lon1) * Math.cos(lat1 / 53.7);
		return Math.sqrt(x * x + y * y);
	}
}
